﻿//-----------------------------------------------------------------------
// <copyright file="ScheduleManager.cs" company="FastNET Services">
//     Copyright (c) 2007-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
//-----------------------------------------------------------------------
#region Usings
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using DDay.iCal;
using DDay.iCal.Components;
using FastNET.CleanAct.Client.Interfaces.EventArguments;
using FastNET.CleanAct.Client.Interfaces.Managers;

#endregion

namespace FastNET.CleanAct.Client.ClientEngine.Managers.Schedule
{
    /// <summary>
    /// Management class for handling all schedule related processing
    /// </summary>
    internal class ScheduleManager : ThreadedManager, IScheduleManager
    {
        #region Variables

        private readonly Dictionary<Guid, iCalendar> _calendars;
        private readonly List<ScheduleItem> _nextEvents;
        private readonly object _lockObject;

        #endregion

        #region CTor/DTor

        /// <summary>
        /// Initializes a new instance of the ScheduleManager class
        /// </summary>
        public ScheduleManager()
        {
            _calendars = new Dictionary<Guid, iCalendar>();
            _nextEvents = new List<ScheduleItem>();
            _lockObject = new object();
        }

        #endregion

        #region Public Events

        /// <summary>
        /// Event used when a schedule is ready to be run
        /// </summary>
        public event EventHandler<ScheduleFiredEventArgs> ScheduleFired;

        #endregion

        #region Protected Overrided Properties

        /// <summary>
        /// Gets the name for the background management thread
        /// </summary>
        protected override string ThreadName
        {
            get
            {
                return "CleanAct Schedule Manager";
            }
        }

        /// <summary>
        /// Gets the priority for the background management thread
        /// </summary>
        protected override ThreadPriority ThreadPriority
        {
            get
            {
                return ThreadPriority.BelowNormal;
            }
        }

        #endregion

        #region Public Schedule Management Methods

        /// <summary>
        /// Clears all schedules from the manager and all queued run times
        /// </summary>
        public void ClearSchedules()
        {
            lock (_lockObject)
            {
                _calendars.Clear();
                _nextEvents.Clear();
            }
        }

        /// <summary>
        /// Loads an iCalendar based schedule into the <see cref="IScheduleManager"/> to be managed
        /// </summary>
        /// <param name="scheduleID">ID for this schedule</param>
        /// <param name="scheduleContent">iCalendar based text for the schedule</param>
        public void LoadSchedule(Guid scheduleID, string scheduleContent)
        {
            StringReader stream = new StringReader(scheduleContent);
            iCalendar calendar = iCalendar.LoadFromStream(stream);

            lock (_lockObject)
            {
                _calendars.Add(scheduleID, calendar);
            }

            // We want to calculate the run time on another thread so we do not block anything
            ThreadPool.QueueUserWorkItem(CalculateScheduleRun, scheduleID);
        }

        /// <summary>
        /// Removes the specified schedule by ID from the manager and any associated queued run times
        /// </summary>
        /// <param name="scheduleID"></param>
        public void UnloadSchedule(Guid scheduleID)
        {
            lock (_lockObject)
            {
                _calendars.Remove(scheduleID);
                _nextEvents.RemoveAll(i => i.ScheduleID == scheduleID);
            }
        }

        #endregion

        #region Protected Event Raisers

        /// <summary>
        /// Event Raiser for the <see cref="ScheduleFired"/> event
        /// </summary>
        /// <param name="e">A <see cref="ScheduleFiredEventArgs"/> with information on the event</param>
        protected void OnScheduleFired(ScheduleFiredEventArgs e)
        {
            if (ScheduleFired != null)
                ScheduleFired.Invoke(this, e);
        }

        #endregion

        #region Private Static Helper Methods

        /// <summary>
        /// Static helper method to take an <see cref="iCalendar"/> and get the next dates the schedule is to occur for this day
        /// </summary>
        /// <param name="calendar">An <see cref="iCalendar"/> to generate the next occurrence from</param>
        /// <returns>A list of <see cref="DateTime"/> with all the next times to run for today</returns>
        private static List<DateTime> GenerateNextOccurrences(iCalendar calendar)
        {
            DateTime beginDate = DateTime.UtcNow; // Current Time
            DateTime endDate = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1); // Real End of Day
            List<DateTime> dateTimes = new List<DateTime>();
            List<Occurrence> occurrences = calendar.GetOccurrences<Event>(beginDate, endDate);

            // Add all the occurrences to our return list
            foreach (Occurrence occurrence in occurrences)
            {
                dateTimes.Add(occurrence.Period.StartTime.Value);
            }

            return dateTimes;
        }

        #endregion

        #region Private Manager Thread Functions

        /// <summary>
        /// Background thread cycle method
        /// </summary>
        protected override void ProcessThreadTasks()
        {
            // Run through our list of run times and fire any needing to be fired
            CheckSchedules();
        }

        #endregion

        #region Private Thread Cycle Methods

        /// <summary>
        /// Cycle method for checking schedule dates to run
        /// </summary>
        private void CheckSchedules()
        {
            List<ScheduleItem> localCopy = new List<ScheduleItem>();

            lock (_lockObject)
            {
                localCopy.AddRange(_nextEvents);
            }

            // Go through our copy of the list
            foreach (ScheduleItem item in localCopy)
            {
                if (item.StartTime > DateTime.UtcNow)
                {
                    continue;
                }

                OnScheduleFired(new ScheduleFiredEventArgs(item.ScheduleID));

                // We want to calculate the run time on another thread so we do not block anything
                ThreadPool.QueueUserWorkItem(CalculateScheduleRun, item.ScheduleID);
            }
        }

        #endregion

        #region Private Threaded Helpers

        /// <summary>
        /// Callback thread method for generating the next occurrences for the schedule specified
        /// </summary>
        /// <param name="state">Guid containing the schedule ID to generate</param>
        private void CalculateScheduleRun(object state)
        {
            Guid scheduleID = (Guid)state;
            List<DateTime> nextOccurrences = GenerateNextOccurrences(_calendars[scheduleID]);

            if (nextOccurrences.Count <= 0)
            {
                return;
            }

            foreach (DateTime date in nextOccurrences)
            {
                ScheduleItem runTime = new ScheduleItem
                                       {
                                           ScheduleID = scheduleID,
                                           StartTime = date
                                       };

                lock (_lockObject)
                {
                    _nextEvents.Add(runTime);
                }
            }
        }

        #endregion

        #region Private Structures

        /// <summary>
        /// Basic structure for a run time of a schedule
        /// </summary>
        private struct ScheduleItem
        {
            /// <summary>
            /// The Schedule ID for the item
            /// </summary>
            public Guid ScheduleID;

            /// <summary>
            /// The DateTime to run the item
            /// </summary>
            public DateTime StartTime;
        }

        #endregion
    }
}
